/*
 * Copyright 2007-2010 Sun Microsystems, Inc.
 *
 * This file is part of Project Darkstar Server.
 *
 * Project Darkstar Server is free software: you can redistribute it
 * and/or modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation and
 * distributed hereunder to you.
 *
 * Project Darkstar Server is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * --
 */

package com.avalon;

import java.util.Properties;
import java.util.Set;
import java.util.HashSet;
import java.util.Enumeration;
import java.io.InputStream;
import java.io.IOException;

/**
 * Extension of {@link Properties} that provides automatic support for
 * substitutions of variables. For example:
 * <p>
 * 
 * PROP1=value<br>
 * PROP2=${PROP1}<br>
 * <br>
 * 
 * When loading the above set of properties from a file, PROP2 will
 * automatically substitute PROP1 for its value.
 * <p>
 * 
 * Additional usage requirements:
 * <ul>
 * <li>When properties are provided from a file or {@code InputStream} of some
 * sort, properties can appear in any order and all values will be interpolated
 * as expected.</li>
 * <li>If a property is set using the
 * {@link #setProperty(java.lang.String, java.lang.String)} method, it will
 * <em>not</em> automatically cause its value to be interpolated in other
 * properties where it is included.</li>
 * <li>An automatically interpolated value of another property can be included
 * in a property by surrounding the key of the value with the start delimiter of
 * '${' and the end delimiter of '}'.</li>
 * <li>Property keys can contain any characters except for the end delimiter of
 * '}'. Inclusion of this character via an escape sequence is not supported.</li>
 * </ul>
 */
public class SubstitutionProperties extends Properties {

	private static final long serialVersionUID = 1L;
	private static final String START_KEY = "${";
	private static final String END_KEY = "}";

	/**
	 * Creates an empty property list with no defaults.
	 */
	public SubstitutionProperties()
	{
		super();
	}

	/**
	 * Creates an empty property list with a default backing of properties. When
	 * initialized with this constructor, any properties in the default backing
	 * that contain substitutable values (i.e. ${PROPNAME}) are replaced and set
	 * with their interpolated value in the new {@code SubstitutionProperties}
	 * object.
	 * 
	 * @param p
	 *            default properties
	 */
	public SubstitutionProperties(Properties p)
	{
		super(p);
		replaceAll();
	}

	/**
	 * Load properties from an {@code InputStream}. The properties are loaded by
	 * calling {@code super.load(inStream);} Additionally, the loaded properties
	 * are filtered and any properties that contain substitutable variables
	 * (i.e. ${PROPNAME}) are replaced and set with their interpolated value.
	 * 
	 * @param inStream
	 *            the input stream to load the properties from
	 * @throws IOException
	 *             if an error occurred while reading from the stream
	 * @see java.util.Properties#load(java.io.InputStream)
	 */
	public void load(InputStream inStream) throws IOException
	{
		super.load(inStream);
		replaceAll();
	}

	/**
	 * Load properties from an XML file specified by the given
	 * {@code InputStream}. The properties are loaded by calling
	 * {@code super.loadFromXML(inStream);} Additionally, the loaded properties
	 * are filtered and any properties that contain substitutable variables
	 * (i.e. ${PROPNAME}) are replaced and set with their interpolated value.
	 * 
	 * @param inStream
	 *            the input stream to load the properties from
	 * @throws IOException
	 *             if an error occurred while reading from the stream
	 * @see java.util.Properties#loadFromXML(java.io.InputStream)
	 */
	public void loadFromXML(InputStream inStream) throws IOException
	{
		super.load(inStream);
		replaceAll();
	}

	/**
	 * Sets the given property by calling the
	 * {@code super.setProperty(name, value)} method. Additionally, the property
	 * is filtered and if it contains subtitutable variables (i.e. ${PROPNAME}),
	 * they are replaced and set with their interpolated value.
	 * 
	 * @param name
	 *            the name of the property
	 * @param value
	 *            the value of the property
	 * @return the previous value of the property, or {@code null} if it was not
	 *         set
	 * @see java.util.Properties#setProperty(java.lang.String, java.lang.String)
	 */
	public Object setProperty(String name, String value)
	{
		String prev = super.getProperty(name);
		super.setProperty(name, value);
		replace(name, new HashSet<String>());
		return prev;
	}

	/**
	 * Walks through each of the properties and replaces any instances of
	 * ${PROPNAME} with the value of PROPNAME if it exists. If such a string is
	 * found in a property and and the property to lookup does not exist, it is
	 * replaced with the empty string.
	 */
	private void replaceAll()
	{
		for (Enumeration<?> e = this.propertyNames(); e.hasMoreElements();)
		{
			String property = (String) e.nextElement();
			replace(property, new HashSet<String>());
		}
	}

	/**
	 * Replaces any instances of ${PROPNAME} in the property with key propName
	 * with the value of PROPNAME if it exists.
	 * 
	 * @param propName
	 *            property name to replace
	 * @param alreadyUsed
	 *            set of properties currently in the state of being interpolated
	 *            so that we can detect loops
	 * @throws IllegalStateException
	 *             if a loop is detected during substitution or if a value
	 *             contains an opening key for substitution '${' but not a
	 *             closing key '}'
	 */
	private String replace(String propName, Set<String> beingInterpolated)
	{
		beingInterpolated.add(propName);
		String propValue = super.getProperty(propName);
		if (propValue == null || propValue.equals(""))
		{
			return "";
		}

		// walk through the value, building a new string and replacing
		// properties as we go
		StringBuilder newValue = new StringBuilder(propValue.length());
		int currentIndex = 0;
		for (int startIndex = propValue.indexOf(START_KEY); startIndex != -1; startIndex = propValue.indexOf(START_KEY,
				currentIndex))
		{
			newValue.append(propValue.substring(currentIndex, startIndex));
			currentIndex = propValue.indexOf(END_KEY, startIndex);
			if (currentIndex != -1)
			{
				String subPropName = propValue.substring(startIndex + 2, currentIndex++);
				if (beingInterpolated.contains(subPropName))
				{
					throw new IllegalStateException("loop detected when interpolating property : " + propName);
				}
				newValue.append(replace(subPropName, beingInterpolated));
			} else
			{
				throw new IllegalArgumentException("illegal property name, '" + END_KEY + "' not found "
						+ "when interpolating property : " + propName);
			}
		}
		newValue.append(propValue.substring(currentIndex, propValue.length()));

		beingInterpolated.remove(propName);
		super.setProperty(propName, newValue.toString());
		return newValue.toString();
	}
}
